Skip to content

perf(core): acquire snapshots concurrently - #2011

Merged
Nikhil (shadowfax92) merged 11 commits into
mainfrom
feat/snapshot-concurrent-acquisition
Jul 23, 2026
Merged

perf(core): acquire snapshots concurrently#2011
Nikhil (shadowfax92) merged 11 commits into
mainfrom
feat/snapshot-concurrent-acquisition

Conversation

@shadowfax92

Copy link
Copy Markdown
Contributor

Summary

  • overlap AX, cursor, validation, and sibling-frame snapshot acquisition under one capture-local eight-request CDP budget
  • batch cursor handle collection and main-world resolution while preserving page attributes and cancellation cleanup
  • retain deterministic reverse iframe assembly/ref allocation, retries, rollback, and tabs.new behavior, with stage timing

Design decisions

  • reuse the existing Tokio runtime; each permit covers one pending CDP request only, with no custom pool or global CDP scheduler
  • keep acquisition immutable/concurrent and rendering/ref mutation sequential in the historical reverse stitch order
  • use target-session default worlds for root/OOPIF frames and resolved Document wrappers for same-process frames; inherit nested target sessions and post-validate child loaders

Test plan

  • cd packages/browseros-agent && cargo fmt --all --check
  • cd packages/browseros-agent && cargo clippy --workspace --all-targets -- -D warnings
  • cd packages/browseros-agent && cargo test --workspace
  • live Chrome exact cursor-asset check for page-assigned onclick in same-process iframe and OOPIF

@shadowfax92

Copy link
Copy Markdown
Contributor Author

Greptile (@greptileai) review

@shadowfax92

Copy link
Copy Markdown
Contributor Author

Claude (@claude) review

@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Claude finished Nikhil (@shadowfax92)'s task in 4m 22s —— View job


Review: concurrent snapshot acquisition

Went through observer/acquisition.rs (new, 2133 lines), the observer.rs orchestration changes, frames.rs, and the cursor-augment.js marker-based rework. This is a large but well-structured refactor — solid design overall.

What's good

  • The SnapshotBudget semaphore model (observer/acquisition.rs:100-132) is sound: a permit is held only for the duration of a single pending CDP request/response and dropped immediately after, so there's no scenario where a future holds a permit while blocked waiting on another future that itself needs a permit — no deadlock risk from the shared 8-request cap.
  • Reverse iframe assembly order is correctly preserved: acquire_child_frames fans out unordered via FuturesUnordered but re-sorts by stitch_index before returning (observer/acquisition.rs:216-224), and the caller in observer.rs iterates .rev() to restore the historical ref-numbering order. This is exactly the kind of thing that's easy to get subtly wrong in a concurrency refactor, and it's handled carefully with a test (observer_acquires_siblings_out_of_order_but_assembles_reverse_stitch_order).
  • Cursor marker cleanup on cancellation (CursorCleanup's Drop impl, observer/acquisition.rs:485-517) correctly detaches a cleanup task via tokio::runtime::Handle::try_current() so task.abort() mid-scan doesn't leak the object group or leave a marker attribute on the live page. Covered by cancelling_cursor_resolution_still_cleans_marker_and_object_group and cancelling_document_resolution_still_releases_its_object_group.
  • The find_cursor_hits batching (single getProperties call instead of one round-trip per candidate) plus the sparse-marker/compact-candidate index reconciliation is non-trivial but well tested against out-of-order describeNode completions and vanished candidates.
  • frames.rs's new same_process_sessions cache correctly inherits the nearest parent's target session (so a same-process child under an OOPIF still evaluates in the OOPIF's session), and is cleaned up alongside oopif_sessions on session detach.
  • Test coverage is unusually thorough for a concurrency change — explicit gated/interleaved connections to force overlap, verify budget caps (cursor_describe_requests_never_exceed_capture_budget), and check cancellation cleanup paths.

Minor findings (non-blocking)

  1. Trace outcome mislabels expected cycle-skips as "partial". In Observer::acquire_child_frames (observer/acquisition.rs:225-231):

    let outcome = if acquired.len() == stitches.len()
        && acquired.iter().all(|child| child.result.is_ok())
    { "success" } else { "partial" };

    A child frame that's legitimately skipped because it's already in visited (cycle detection) also makes acquired.len() < stitches.len(), so a normal, expected skip gets traced as "partial" alongside genuine failures. This is just an observability nit (the snapshot_stage=sibling_acquisition trace could be misread as a partial failure when nothing actually went wrong), not a functional bug.
    Fix this →

  2. js_protocol.json's new getProperties/PropertyDescriptor schema entries appear unused. acquisition.rs calls Runtime.getProperties via budget.send::<GetPropertiesResult>(...) using its own hand-rolled GetPropertiesResult/PropertyDescriptor structs (observer/acquisition.rs:410-419), not the codegen'd bindings the protocol JSON change would produce in browseros-cdp. A repo-wide search turns up no other caller of the generated get_properties binding. Worth confirming this schema addition is intentional (e.g. for protocol-completeness/future typed use) rather than a leftover from an earlier approach — harmless either way, just extra surface to maintain.

Not verified

  • I did static review only; I didn't run cargo fmt/clippy/cargo test locally (sandboxed environment). CI shows "Code Quality" passed and "Tests" is still in progress as of this review — worth confirming it goes green before merge.
  • Didn't independently verify the live-Chrome cursor-asset behavior mentioned in the test plan (same-process iframe / OOPIF onclick) — that's inherently something only a live browser run can confirm.

Nothing here blocks merging; both findings are low-severity and non-functional.

@shadowfax92

Copy link
Copy Markdown
Contributor Author

DeepScan’s UNUSED_EXPR finding was a false positive for the cursor asset: CDP Runtime.callFunctionOn consumes the file as an uninvoked functionDeclaration. Added the documented rule-specific suppression plus a purpose comment in e226a2a; the 16 acquisition tests, formatting, diff check, and Biome hook are green.

@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Snapshot acquisition now overlaps accessibility, cursor, document-validation, and sibling-frame CDP work while preserving deterministic assembly.

  • Adds an eight-request capture-local CDP concurrency budget and stage timing.
  • Batches cursor candidate resolution with collision-safe markers and cancellation cleanup.
  • Caches inherited target sessions for nested same-process frames.
  • Extends the Runtime protocol and adds concurrency, retry, cleanup, and frame-depth tests.

Confidence Score: 2/5

The reconnect-sensitive frame-session cache and stale child-frame commit path must be corrected before this PR is safe to merge.

Prior-epoch sessions remain selectable after reconnect, and child-frame loader changes discard only ref identity while retaining AX data and cursor hits acquired from the replaced document.

packages/browseros-agent/crates/browseros-core/src/frames.rs, packages/browseros-agent/crates/browseros-core/src/observer/acquisition.rs

Important Files Changed

Filename Overview
packages/browseros-agent/crates/browseros-core/src/observer/acquisition.rs Introduces bounded concurrent CDP acquisition and cleanup, but child-loader changes still allow previously acquired child data to be committed.
packages/browseros-agent/crates/browseros-core/src/observer.rs Reworks capture orchestration to acquire immutable frame data concurrently and assemble snapshots deterministically.
packages/browseros-agent/crates/browseros-core/src/frames.rs Adds inherited-session caching for same-process frames, but the cache is not invalidated when the CDP connection epoch changes.
packages/browseros-agent/crates/browseros-core/src/assets/cursor-augment.js Parameterizes cursor markers and returns collision-aware candidate batches.
packages/browseros-agent/crates/browseros-cdp/protocol/js_protocol.json Adds the Runtime.getProperties command and its property descriptor shape for batched remote-object collection.

Sequence Diagram

sequenceDiagram
    participant Observer
    participant Budget as SnapshotBudget
    participant AX as Accessibility
    participant Runtime
    participant Frames as Child Frames
    Observer->>Budget: Begin capture
    par Acquire AX tree
        Budget->>AX: getFullAXTree
        AX-->>Observer: nodes
    and Scan cursor candidates
        Budget->>Runtime: evaluate/callFunctionOn
        Runtime-->>Observer: candidate handles
    and Validate document
        Budget->>Frames: getFrameTree
        Frames-->>Observer: loader identities
    end
    par Acquire sibling frames
        Observer->>Frames: acquire child A
    and
        Observer->>Frames: acquire child B
    end
    Observer->>Observer: Restore stitch order
    Observer->>Observer: Sequentially render and assign refs
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/browseros-agent/crates/browseros-core/src/frames.rs:97-102
**Stale sessions survive reconnects**

When CDP reconnects after a same-process frame session has been cached, this lookup returns the prior connection epoch's session because only `PageManager` clears its sessions. Resolving an existing frame ref then sends click, typing, scrolling, or annotation commands through an invalid session and fails with an unknown or detached-session protocol error.

### Issue 2 of 2
packages/browseros-agent/crates/browseros-core/src/observer/acquisition.rs:294-296
**Changed child snapshots remain committable**

When a child frame navigates after its AX data is acquired, revalidation reports the changed loader but only clears `document_id`; the old nodes and cursor hits are still returned and committed because capture-level stability checks cover only the main frame. The resulting snapshot exposes stale child content and actionable refs from the replaced document.

Reviews (1): Last reviewed commit: "fix: suppress intentional cursor asset e..." | Re-trigger Greptile

Comment thread packages/browseros-agent/crates/browseros-core/src/frames.rs
@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Concurrent snapshot acquisition is introduced while preserving sequential rendering semantics.

  • Adds an eight-request capture-local CDP budget and overlaps accessibility-tree, cursor, document-validation, and sibling-frame acquisition.
  • Batches cursor candidate resolution with collision-safe DOM markers, object-group cleanup, and same-process frame document targeting.
  • Caches inherited sessions for same-process frames and restores deterministic reverse iframe assembly and ref allocation.
  • Extends the Runtime protocol definitions and adds concurrency, retry, cleanup, frame-session, and ordering tests.

Confidence Score: 5/5

The pull request appears safe to merge, with concurrency bounded locally and historical snapshot assembly behavior preserved.

The changed acquisition paths restore deterministic stitch order before sequential ref mutation, isolate cursor markers per capture, clean remote objects on success and cancellation, and retain snapshot retry and child-failure rollback behavior.

Important Files Changed

Filename Overview
packages/browseros-agent/crates/browseros-core/src/observer/acquisition.rs Adds bounded concurrent frame, cursor, and document acquisition with deterministic result ordering and cancellation cleanup.
packages/browseros-agent/crates/browseros-core/src/observer.rs Integrates concurrent acquisition into capture retries while retaining sequential iframe assembly, ref rollback, and stable ordering.
packages/browseros-agent/crates/browseros-core/src/frames.rs Adds inherited session tracking for same-process frames nested beneath page or OOPIF targets.
packages/browseros-agent/crates/browseros-core/src/assets/cursor-augment.js Changes cursor scanning to use capture-specific marker attributes and structured collision results.
packages/browseros-agent/crates/browseros-cdp/protocol/js_protocol.json Adds the Runtime property-descriptor and getProperties definitions required for batched cursor handle collection.

Sequence Diagram

sequenceDiagram
    participant Observer
    participant Budget as SnapshotBudget
    participant Frames as FrameRegistry
    participant CDP
    participant Renderer

    Observer->>Budget: Read initial frame tree
    par Acquire AX tree
        Budget->>CDP: Accessibility.getFullAXTree
    and Acquire cursor candidates
        Budget->>CDP: Runtime.evaluate/callFunctionOn
        Budget->>CDP: Runtime.getProperties
        Budget->>CDP: DOM.describeNode (bounded)
    and Validate document
        Budget->>CDP: Page.getFrameTree
    end
    Observer->>Renderer: Render immutable frame inputs
    Renderer-->>Observer: Iframe stitches
    par Acquire sibling frames
        Observer->>Frames: Resolve inherited target sessions
        Frames->>CDP: Acquire child frame inputs
    end
    Observer->>Renderer: Assemble children in reverse stitch order
    Observer->>Budget: Read final frame tree
    Observer-->>Observer: Commit snapshot or retry
Loading

Reviews (2): Last reviewed commit: "fix: suppress intentional cursor asset e..." | Re-trigger Greptile

@shadowfax92

Copy link
Copy Markdown
Contributor Author

Addressed both non-blocking Claude findings in 71871c9: expected visited-frame cycle skips no longer label sibling acquisition as partial, and cursor acquisition now deserializes Runtime.getProperties through the generated protocol result type. Formatting, workspace Clippy with warnings denied, and the full workspace test suite are green.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Tests passed — 1694/1698

Suite Passed Failed Skipped
agent 350/350 0 0
build 34/34 0 0
claw-app 214/214 0 0
⚠️ claw-mcp 0/0 0 0
⚠️ claw-onboard 0/0 0 0
⚠️ claw-server-rust-quality 0/0 0 0
⚠️ claw-server-rust 0/0 0 0
server-agent 314/314 0 0
server-api 171/171 0 0
server-browser 10/10 0 0
server-integration 10/10 0 0
server-lib 299/300 0 1
server-root 38/41 0 3
server-tools 254/254 0 0

View workflow run

@shadowfax92
Nikhil (shadowfax92) merged commit 161d39d into main Jul 23, 2026
22 checks passed
Dani Akash (DaniAkash) added a commit that referenced this pull request Jul 24, 2026
* chore(deps): bump golang.org/x/crypto to v0.52.0 in apps/cli (#1994)

Refresh go.sum and the x/sys transitive dependency.

* chore(deps): bump the uv group across 1 directory with 5 updates (#1995)

Bumps the uv group with 5 updates in the /packages/browseros directory:

| Package | From | To |
| --- | --- | --- |
| [requests](https://github.com/psf/requests) | `2.32.5` | `2.33.0` |
| [python-dotenv](https://github.com/theskumar/python-dotenv) | `1.2.1` | `1.2.2` |
| [cryptography](https://github.com/pyca/cryptography) | `46.0.3` | `48.0.1` |
| [idna](https://github.com/kjd/idna) | `3.11` | `3.15` |
| [urllib3](https://github.com/urllib3/urllib3) | `2.5.0` | `2.7.0` |



Updates `requests` from 2.32.5 to 2.33.0
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](psf/requests@v2.32.5...v2.33.0)

Updates `python-dotenv` from 1.2.1 to 1.2.2
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md)
- [Commits](theskumar/python-dotenv@v1.2.1...v1.2.2)

Updates `cryptography` from 46.0.3 to 48.0.1
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](pyca/cryptography@46.0.3...48.0.1)

Updates `idna` from 3.11 to 3.15
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](kjd/idna@v3.11...v3.15)

Updates `urllib3` from 2.5.0 to 2.7.0
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](urllib3/urllib3@2.5.0...2.7.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.33.0
  dependency-type: direct:production
  dependency-group: uv
- dependency-name: python-dotenv
  dependency-version: 1.2.2
  dependency-type: direct:production
  dependency-group: uv
- dependency-name: cryptography
  dependency-version: 48.0.1
  dependency-type: direct:production
  dependency-group: uv
- dependency-name: idna
  dependency-version: '3.15'
  dependency-type: indirect
  dependency-group: uv
- dependency-name: urllib3
  dependency-version: 2.7.0
  dependency-type: indirect
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): pin transitive dependency resolutions in browseros-agent (#1997)

Refresh a batch of pinned transitive resolutions to their current patch
releases via overrides and update bun.lock.

* Revert "fix(claw-app): scale Audit tab navigation (#1987)" (#1998)

This reverts commit ee46ec4.

* feat(claw): rewrite BrowserClaw harness skill (#1999)

Frontmatter description now carries the standing instruction — default to
BrowserClaw for browser work unprompted, over in-app browser surfaces —
and the body becomes a compact operating manual: session etiquette,
task-owned tabs, the snapshot -> act -> verify loop, read/grep and tool
escalation, failure handling, and the untrusted-page-content rule.
Validation test probes and line budget updated to match.

* fix(agent): reconcile aliased skill targets by identity (#2001)

* fix(agent): reconcile aliased skill targets by identity

* fix(claw): use app resources in local launchers

* test(claw): assert physical skill manifest paths

* fix: address review findings for browserclaw-skill-target-identity

* feat(claw): show total tokens consumed on Audit surfaces (#2000)

* feat(claw): show total tokens consumed on Audit surfaces

Materialize per-session token estimates (input+output, summed across
dispatches) onto the tasks table via recompute_task, so the total is
available live and not only from the post-teardown efficiency projection.
Expose it through the canonical OpenAPI path as an optional SessionSummary
tokenUsage field, present only when the session has dispatches and every one
carries token-estimator v1 (legacy/unmeasured sessions omit it).

Both Audit surfaces render it from the one shared field: a compact Tokens
column on the session list (exact count on hover, em dash when unmeasured)
and a Tokens entry on the session-detail summary card.

* fix(claw): sort unmeasured token totals last in both directions

The Tokens-column accessor mapped unmeasured sessions to 0, which only
sank them on a descending sort — an ascending sort surfaced them ahead of
every measured session. Return undefined and use tanstack's
sortUndefined:'last' so unmeasured rows stay at the bottom either way.

* fix(claw): hide tokens in audit list (#2002)

* fix(claw): ground screenshot token baseline (#2003)

* fix(claw): ground screenshot token baseline

* test(claw): update cockpit efficiency expectation

* test(claw): preserve signed savings coverage

* feat(claw): move audit observation off the tool path (#2005)

* refactor(claw): separate tool observers from effects

* feat(claw): queue audit observations in bounded worker

* fix(claw): flush coalesced audit previews on session close

* test(claw): verify async audit shutdown lifecycle

* fix: address review findings for 260723-1154_async_audit_observer

* fix(claw): sum tool durations for human time saved (#2004)

* fix(claw): sum tool durations for time saved

* fix(claw): preserve retained efficiency history

* fix(claw): guard efficiency backfill from session reuse

* fix(claw): stop cockpit saved-stats labels from overlapping (#2006)

Both the "used" and "screenshot-first" token labels were absolutely
positioned inside the same overflow-hidden budget track. Around the
mid-range their text occupied the same pixels and painted through each
other (e.g. "used 63.2K" over "112.3K").

Lift both numbers into a legend row above the bar — a flex
justify-between row that wraps to two lines before the labels could ever
meet — and keep the track as a pure gauge: accent fill = what
BrowserClaw used, striped remainder = what a screenshot-first agent
would have spent. The bar carries no text now, so nothing can collide or
clip at any used/total ratio or label length.

Accessibility, reduced-motion (the pulsing dot keeps motion-reduce),
theme tokens, and the stats/API contract are unchanged. Tests assert the
structural invariant (no labels inside the bounded bar) across low, mid,
and full ratios, and the fill width replaces the old marker-position
assertions.

* refactor(patches): retire hidden window implementation (#2007)

* refactor(patches): retire hidden window implementation

* fix(patches): preserve single-close window teardown

* chore: sync internal-docs submodule (#2008)

Co-authored-by: browseros-bot <bot@browseros.ai>

* chore: sync internal-docs submodule (#2009)

Co-authored-by: browseros-bot <bot@browseros.ai>

* fix(claw): use session duration for human time saved (#2010)

* fix(claw): use session duration for saved time

* fix(claw): backfill saved time from task durations

* perf(core): acquire snapshots concurrently (#2011)

* perf(core): batch cursor backend-node resolution

* refactor(core): separate snapshot frame acquisition

* perf(core): acquire snapshot sibling frames concurrently

* perf(core): trace concurrent snapshot capture

* fix: address review findings for snapshot-concurrent-acquisition

* fix: address review findings for snapshot-concurrent-acquisition

* fix: address review findings for snapshot-concurrent-acquisition

* fix: suppress intentional cursor asset expression

* fix(core): clear frame sessions after reconnect

* fix(core): address snapshot PR review notes

* feat(claw-app): default replays to 2x (#2015)

* chore: refresh TypeScript CDP and retire hidden surfaces (#2014)

* chore(cdp): refresh TypeScript protocol definitions

* refactor(server): use background pages for scheduled tasks

* refactor(browser-core): retire hidden page and window behavior

* refactor(browser-mcp): remove hidden tab and window controls

* test: repair TypeScript verification baselines

* test(claw-app): prepare WXT aliases before tests

* fix(claw): exclude red from automatic tab-group colors (#2016)

* fix(claw): exclude red from automatic tab groups

* test(claw): update automatic tab color fixture

* test(claw): cover snapshot concurrency contracts (#2017)

* test(claw): add snapshot concurrency fixtures

* test(claw): cover live cursor snapshot acquisition

* test(claw): cover concurrent iframe snapshot contracts

* test(claw): stress overlapping snapshot contracts

* fix: address review findings for snapshot-concurrency-contract-tests

* fix: address review findings for snapshot-concurrency-contract-tests

* refactor: refresh Rust CDP and retire hidden windows (#2013)

* chore(cdp): sync Rust protocol from Chromium

* refactor(core): retire hidden window state

* refactor(mcp): remove hidden window controls

* refactor(server): drop hidden window runtime references

* test(core): align getProperties fixtures with protocol

* test(mcp): retire hidden conformance cases

* chore(release): build v0.48.3 [skip ci]

Automated nightly macOS version bump.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nikhil <nikhilsv92@gmail.com>
Co-authored-by: browseros-bot <bot@browseros.ai>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Nikhil (shadowfax92) added a commit that referenced this pull request Jul 30, 2026
* feat(code-mode): collapse to one audited, ownership-aware script tool (#1996)

* feat(claw-server-rust): add parent_dispatch_id to tool_dispatches

Adds the audit-hierarchy column that links a primitive dispatch to the
script dispatch it ran inside. A nullable parent_dispatch_id lands on
the tool_dispatches entity and RecordToolDispatchInput, is written by
the audit insert, and ships as a named migration
(m0008_add_parent_dispatch_id) following the existing nullable-column
pattern. Schema and migration-count tests are updated for the new
column and eighth migration.

No behavior change yet: every current dispatch records
parent_dispatch_id = None. The script inner-call hook that populates it
for run/execute primitives lands in a follow-up commit.

* feat(browseros-mcp): add InnerCallHook seam to ToolCtx

Defines the InnerCallHook trait plus InnerCallRecord in the framework:
a host-injected hook a script tool calls around each browser primitive
to enforce per-primitive ownership (authorize) and record the primitive
as a child audit row (record). browseros-mcp cannot reach the host's
guards or audit store, so the host implements this and passes it in
through the new optional ToolCtx.inner_call_hook field.

No behavior change: the field defaults to None at every construction
site, so nothing invokes the hook yet. The run bridge starts calling it
in a follow-up commit.

* feat(browseros-mcp): run bridge invokes the inner-call hook per primitive

BrowserBridge now calls the injected InnerCallHook around every browser
primitive: authorize before dispatch (a rejection throws into the
script and skips execution) and record after. The bridge computes the
target page from the method and arguments so the host can check
ownership of the exact page a primitive touches. The match body moves
into a dispatch() helper wrapped by call().

Threaded through execute_run -> execute_quickjs -> install_globals from
ctx.inner_call_hook, so it stays None (a no-op) unless the host injects
it. Covered by a mock-hook test asserting authorize/record fire with
the right page, and that a rejection blocks the primitive before
dispatch and records nothing.

* feat(claw-server-rust): enforce ownership and audit script inner calls

Implements the host InnerCallHook (ScriptInnerCallHook) and injects it
for script tools at the dispatch site. Each browser primitive a run
script executes is now:

- ownership-checked: a page owned by another conversation is rejected
  with the same message the page_ownership guard uses, closing the
  isolation gap where a script could drive another agent's tab. Pages a
  script creates itself are unclaimed and stay usable; rejecting
  unclaimed pages at full guard parity needs claiming script-created
  pages and is a follow-up.
- audited: recorded as a child tool_dispatches row carrying
  parent_dispatch_id = the script's dispatch id, so the cockpit can
  attribute and replay the primitives inside a run instead of seeing a
  black box.

The hook is injected only for ARBITRARY_SCRIPT_TOOLS with a resolved
identity; every other dispatch is unchanged. Covered by tests for the
foreign-page rejection and the linked child-row write.

* fix(claw-server-rust): satisfy clippy and integration-test constructors

Recovers poisoned test locks via PoisonError::into_inner and replaces
expect/unwrap in the new hook tests with ? and ok_or_else to satisfy the
workspace clippy denial. Adds the parent_dispatch_id field to the two
integration-test RecordToolDispatchInput constructors that the lib build
did not cover.

* feat(browseros-mcp): bring the run SDK to tool parity via handler routing

Extends the browser SDK with the capabilities the session-direct bridge
did not cover: read, grep, wait, screenshot, evaluate, download, pdf,
upload, tabGroups, windows. Rather than reimplement each, a `tool:`
bridge method dispatches through the real tool handler with
execute_tool, so the script reaches full parity with the granular tool
set and each primitive reuses that tool's argument validation and
output. The SDK method supplies the page argument; opts mirror the
same-named tool's arguments.

The bridge now holds the full ToolCtx (session-direct primitives use
ctx.session, tool-backed ones pass ctx to execute_tool, the inner-call
hook lives at ctx.inner_call_hook), so the ownership + audit hook wraps
tool-backed primitives too. Covered by a test routing browser.windows
through the handler and asserting the hook authorizes and records it.

* feat(claw-server-rust): collapse the MCP surface to the code-mode tool

The host now exposes only the script tool plus name_session; the
granular browser tools are filtered out of the catalog it serves.
Because listing, lookup, and call all read the same catalog, filtering
it both hides and blocks the granular tools. Their implementations stay
in the crate as the functions the script bridge dispatches to, so no
capability is lost: every granular tool is reachable inside the browser
SDK. This removes the cheap per-call path whose existence stops agents
from composing and persisting flows.

The operating guide is rewritten for code mode: one tool that runs
JavaScript against the browser SDK, the SDK surface, and the
compose-the-whole-flow-in-one-script loop. Covered by a test asserting
the granular tools are neither listed nor callable while the script
tool and name_session remain.

* feat(claw-server-rust): persistence seam for code-mode helpers

Adds the storage layer the self-healing work builds on: reusable .js a
script saves for a host and a later script loads by name, laid out as
<browserclaw_dir>/helpers/<host>/<name>.js. Provides traversal-safe
list/read/write over that convention, rejecting host or name segments
that could escape the helpers root. The saveHelper primitive and the
hot-load into the script runtime land with the self-healing work; this
is only the seam so that work is additive.

* feat(claw-api): expose script dispatch hierarchy on the audit contract

The cockpit dispatch DTO now carries dispatchKey (this row's stable
ULID) and parentDispatchId (the script dispatch a primitive ran
inside), so the cockpit can group a script's primitives under it and
replay them. Adds the two optional fields to the OpenAPI Dispatch
schema and the generated model, and populates them from the audit row
in the session detail mapper. Backend contract only; the cockpit
frontend rendering of the group is a follow-on in the app.

* refactor(claw-server-rust): hide granular tools from the list, keep them callable

Collapses the surface by filtering listed_tools rather than the whole
catalog, so the agent sees only the script tool plus name_session while
the granular tools stay callable by name. This keeps the server's own
dispatch and the end-to-end tests, which drive tools directly, working,
while still removing the granular tools from what the agent can discover
and reach for. Updates the initialize integration test to expect the
collapsed list and the new code-mode instructions opener.

* fix(claw-server-rust): claim and group pages a code-mode script creates

A script's browser.pages.newPage called the session directly, so it
skipped the ownership-claims and tab-groups effects that a `tabs new`
runs: no page ownership claim, no session-tab window, no tab activity,
and no per-agent tab group. Agents in code mode could step on each
other and on the user's tabs, and the cockpit could not attribute the
tab.

Widens InnerCallHook with on_page_created, which the run bridge fires
after a newPage returns. The host ScriptInnerCallHook now holds the
script's ToolCall and, on page creation, reuses the same effect logic:
record_new_page (extracted from ownership-claims) for the claim, tab
activity, and session-tab window, and run_tab_group_work (from
tab-groups) to ensure the agent's tab group and place the page in it.

This is the keystone fix: the session-tab window it writes is also what
replay attribution and per-tab screenshot selection depend on.

* test(claw-server-rust): verify code-mode tabs open the replay window

Session replays were missing because replay attribution joins a
session's recordings through its session_tabs ownership window, which
code-mode tabs never opened. The keystone fix (claiming pages a script
creates) now opens that window; this locks it with a test that
on_page_created writes the session_tab claim (tab id, target, claimed
window) a code-mode tab needs for its recording to resolve into a
replay. No production change beyond the keystone fix.

* fix(claw-server-rust): capture per-step screenshots for code-mode primitives

Screenshots were missing because the per-dispatch capture runs in the
audit effect, which the script bridge bypasses; only the outer run got
one, and even that failed with no owned tab to shoot. The script hook
now captures a screenshot per page-targeting primitive, keyed to the
child audit row, reusing the audit effect's persist_screenshot (made
shareable, taking the row id). Page-less primitives (windows, tab
groups) are skipped. Combined with the keystone claim, capture now has
an owned tab to select.

* fix(claw-server-rust): report code-mode session liveness and tab activity

A code-mode session showed Idle with zero tabs and lingered in the
cockpit "running" list, because the session touch, tab activity, and
session-tab signals are populated only by the bypassed effects. The
script hook now touches the session per primitive to keep the idle
clock fresh and records tab activity for the touched page, so the
cockpit sees the tab and the session progressing. Recorded before the
per-step screenshot so capture selects the touched tab. The hard
session-end path (transport close, idle sweep) is unchanged.

* fix(browseros-mcp): give the run SDK an exact contract to stop tab churn

Agents opened duplicate tabs and never closed them because the tool
description did not state what each SDK call returns, so the agent
probed at runtime (typeof, Object.keys, getOwnPropertyNames) and
re-opened pages to inspect results. The description now states exact
return shapes (newPage returns a numeric pageId, read returns a
markdown string, and so on), tells the agent not to probe them, to
reuse a pageId across steps, and to close a page with
browser.pages.close when done. Also documents Promise.all for
running independent primitives concurrently.

* fix(claw-server-rust): make code-mode audits read as run-then-primitives

The audit showed a script's primitives before its run row and labelled
them with the internal tool: routing prefix, so it looked like tools
ran without a run. Two fixes: the outer script dispatch is now stamped
with its start time (created_at), so run sorts before the child
primitives it records while executing; and the tool: prefix is stripped
from child rows so they read as the plain capability (read, wait,
screenshot). The full nesting of children under their run, via the
parent_dispatch_id already on the contract, is cockpit-frontend work.

* fix(browseros-mcp): read/grep return text, and correct the SDK contract

The previous description claimed read returns a markdown string, but the
bridge returned the tool's structured metadata ({ format, path,
contentLength }); the agent got an object, saw md.length undefined,
assumed the read failed, and re-opened tabs to debug, which is where the
duplicate tabs came from. run_tool now returns the text for read and
grep, so read is a string as documented. The description is also
corrected where it was wrong: read and grep return text, and wait takes
{ for, value, timeout } (a plain pause is { value: ms }), not { ms }.

* fix(claw-server-rust): order session dispatches by created_at, not write id

The session-detail dispatches were ordered by row id, i.e. write order,
so a script's child primitives (written while it executes) sorted before
the script row (written when it finishes). The earlier created_at fix
stamped the script with its start time but this query ignored it.
Ordering by created_at (id breaks ties) puts a run before the primitives
it contains, so the audit reads as run-then-its-steps.

* fix(claw-server-rust): make the BrowserClaw harness skill a plain pointer

The installed skill still told harnesses to follow a snapshot -> act ->
verify tool loop, which describes the retired granular tools and steers
agents to methods that no longer exist. The skill's only job is to say:
if you need a browser, use BrowserClaw and prefer it over other browser
surfaces. It now does exactly that and defers all how-to to the MCP
initialize instructions and the run tool description, which are the
single source of truth for the code-mode SDK. Test updated for the
pointer shape.

* fix(browseros-mcp): code-mode newPage opens in the background by default

An agent opening a page stole the user's focus: the code-mode bridge
left background unset, and Chrome opens an unspecified tab in the
foreground, whereas the granular tabs-new tool defaults background=true
("open without stealing focus"). Code-mode newPage now defaults to a
background tab, so a working agent does not interrupt the user; an
explicit background:false opens it active, which the agent should do
only when the user asks to bring a tab to the front. Documented in the
SDK description.

* perf(browseros-mcp): steer run scripts to batch and parallelize

Audit review showed agents making one run call per item and processing
serially, so a job spent nearly half its wall-clock in fixed per-page
waits (14s of 30s on the price task). The description now instructs
doing the whole task in as few run calls as possible, looping over all
items in one call, and parallelizing independent work with Promise.all
so N pages cost one wait cycle instead of N, with a worked example.
This is guidance, not a hard limit; the concurrency was already
supported.

* feat(claw-server-rust): advertise multi-agent parallelism in the operating guide

BrowserClaw's isolation model already gives every agent, including a
subagent, its own tabs and tab group, so parallel agents cannot collide,
but the guide did not say so, leaving that ceiling unused. The
instructions now state the isolation guarantee and frame parallelism at
two levels: batch primitives with Promise.all inside one run, and split
a large workload across subagents (when the harness supports them),
since each gets isolated tabs here. The per-agent tab budget stays at
about 5. This lives in the operating guide, not the pointer skill.

* fix(claw-server): tri-bucket ownership view for code-mode pages.list

browser.pages.list() returned a raw, unclassified array of every open
tab, so a code-mode script had no signal that a tab belonged to the user
or another agent. A benign close-all cleanup over that list could close
the user's tabs and empty the window.

Return the same tri-bucket ownership view the granular tabs-list already
produces: each page is tagged ownership "mine" | "user" | "other-agent"
with ownerAgentId and ownerLabel. The engine crate cannot compute
ownership, so annotation goes through a new InnerCallHook.annotate_pages
seam the host implements, reusing the tabs-list-view classifier.

This is classification, not enforcement: page authorization is unchanged
(peer tabs rejected, the user's tabs allowed), so an agent still works on
a user tab when asked. The run description and MCP instructions tell the
agent to act on and close only its own tabs.

* style: apply rustfmt to the code-mode crates

* test(claw-mcp): expect the collapsed code-mode tool catalog

* chore: sync feat/browserclaw-code-mode with main (#2021)

* chore(deps): bump golang.org/x/crypto to v0.52.0 in apps/cli (#1994)

Refresh go.sum and the x/sys transitive dependency.

* chore(deps): bump the uv group across 1 directory with 5 updates (#1995)

Bumps the uv group with 5 updates in the /packages/browseros directory:

| Package | From | To |
| --- | --- | --- |
| [requests](https://github.com/psf/requests) | `2.32.5` | `2.33.0` |
| [python-dotenv](https://github.com/theskumar/python-dotenv) | `1.2.1` | `1.2.2` |
| [cryptography](https://github.com/pyca/cryptography) | `46.0.3` | `48.0.1` |
| [idna](https://github.com/kjd/idna) | `3.11` | `3.15` |
| [urllib3](https://github.com/urllib3/urllib3) | `2.5.0` | `2.7.0` |



Updates `requests` from 2.32.5 to 2.33.0
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](psf/requests@v2.32.5...v2.33.0)

Updates `python-dotenv` from 1.2.1 to 1.2.2
- [Release notes](https://github.com/theskumar/python-dotenv/releases)
- [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md)
- [Commits](theskumar/python-dotenv@v1.2.1...v1.2.2)

Updates `cryptography` from 46.0.3 to 48.0.1
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](pyca/cryptography@46.0.3...48.0.1)

Updates `idna` from 3.11 to 3.15
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](kjd/idna@v3.11...v3.15)

Updates `urllib3` from 2.5.0 to 2.7.0
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](urllib3/urllib3@2.5.0...2.7.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.33.0
  dependency-type: direct:production
  dependency-group: uv
- dependency-name: python-dotenv
  dependency-version: 1.2.2
  dependency-type: direct:production
  dependency-group: uv
- dependency-name: cryptography
  dependency-version: 48.0.1
  dependency-type: direct:production
  dependency-group: uv
- dependency-name: idna
  dependency-version: '3.15'
  dependency-type: indirect
  dependency-group: uv
- dependency-name: urllib3
  dependency-version: 2.7.0
  dependency-type: indirect
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): pin transitive dependency resolutions in browseros-agent (#1997)

Refresh a batch of pinned transitive resolutions to their current patch
releases via overrides and update bun.lock.

* Revert "fix(claw-app): scale Audit tab navigation (#1987)" (#1998)

This reverts commit ee46ec4.

* feat(claw): rewrite BrowserClaw harness skill (#1999)

Frontmatter description now carries the standing instruction — default to
BrowserClaw for browser work unprompted, over in-app browser surfaces —
and the body becomes a compact operating manual: session etiquette,
task-owned tabs, the snapshot -> act -> verify loop, read/grep and tool
escalation, failure handling, and the untrusted-page-content rule.
Validation test probes and line budget updated to match.

* fix(agent): reconcile aliased skill targets by identity (#2001)

* fix(agent): reconcile aliased skill targets by identity

* fix(claw): use app resources in local launchers

* test(claw): assert physical skill manifest paths

* fix: address review findings for browserclaw-skill-target-identity

* feat(claw): show total tokens consumed on Audit surfaces (#2000)

* feat(claw): show total tokens consumed on Audit surfaces

Materialize per-session token estimates (input+output, summed across
dispatches) onto the tasks table via recompute_task, so the total is
available live and not only from the post-teardown efficiency projection.
Expose it through the canonical OpenAPI path as an optional SessionSummary
tokenUsage field, present only when the session has dispatches and every one
carries token-estimator v1 (legacy/unmeasured sessions omit it).

Both Audit surfaces render it from the one shared field: a compact Tokens
column on the session list (exact count on hover, em dash when unmeasured)
and a Tokens entry on the session-detail summary card.

* fix(claw): sort unmeasured token totals last in both directions

The Tokens-column accessor mapped unmeasured sessions to 0, which only
sank them on a descending sort — an ascending sort surfaced them ahead of
every measured session. Return undefined and use tanstack's
sortUndefined:'last' so unmeasured rows stay at the bottom either way.

* fix(claw): hide tokens in audit list (#2002)

* fix(claw): ground screenshot token baseline (#2003)

* fix(claw): ground screenshot token baseline

* test(claw): update cockpit efficiency expectation

* test(claw): preserve signed savings coverage

* feat(claw): move audit observation off the tool path (#2005)

* refactor(claw): separate tool observers from effects

* feat(claw): queue audit observations in bounded worker

* fix(claw): flush coalesced audit previews on session close

* test(claw): verify async audit shutdown lifecycle

* fix: address review findings for 260723-1154_async_audit_observer

* fix(claw): sum tool durations for human time saved (#2004)

* fix(claw): sum tool durations for time saved

* fix(claw): preserve retained efficiency history

* fix(claw): guard efficiency backfill from session reuse

* fix(claw): stop cockpit saved-stats labels from overlapping (#2006)

Both the "used" and "screenshot-first" token labels were absolutely
positioned inside the same overflow-hidden budget track. Around the
mid-range their text occupied the same pixels and painted through each
other (e.g. "used 63.2K" over "112.3K").

Lift both numbers into a legend row above the bar — a flex
justify-between row that wraps to two lines before the labels could ever
meet — and keep the track as a pure gauge: accent fill = what
BrowserClaw used, striped remainder = what a screenshot-first agent
would have spent. The bar carries no text now, so nothing can collide or
clip at any used/total ratio or label length.

Accessibility, reduced-motion (the pulsing dot keeps motion-reduce),
theme tokens, and the stats/API contract are unchanged. Tests assert the
structural invariant (no labels inside the bounded bar) across low, mid,
and full ratios, and the fill width replaces the old marker-position
assertions.

* refactor(patches): retire hidden window implementation (#2007)

* refactor(patches): retire hidden window implementation

* fix(patches): preserve single-close window teardown

* chore: sync internal-docs submodule (#2008)

Co-authored-by: browseros-bot <bot@browseros.ai>

* chore: sync internal-docs submodule (#2009)

Co-authored-by: browseros-bot <bot@browseros.ai>

* fix(claw): use session duration for human time saved (#2010)

* fix(claw): use session duration for saved time

* fix(claw): backfill saved time from task durations

* perf(core): acquire snapshots concurrently (#2011)

* perf(core): batch cursor backend-node resolution

* refactor(core): separate snapshot frame acquisition

* perf(core): acquire snapshot sibling frames concurrently

* perf(core): trace concurrent snapshot capture

* fix: address review findings for snapshot-concurrent-acquisition

* fix: address review findings for snapshot-concurrent-acquisition

* fix: address review findings for snapshot-concurrent-acquisition

* fix: suppress intentional cursor asset expression

* fix(core): clear frame sessions after reconnect

* fix(core): address snapshot PR review notes

* feat(claw-app): default replays to 2x (#2015)

* chore: refresh TypeScript CDP and retire hidden surfaces (#2014)

* chore(cdp): refresh TypeScript protocol definitions

* refactor(server): use background pages for scheduled tasks

* refactor(browser-core): retire hidden page and window behavior

* refactor(browser-mcp): remove hidden tab and window controls

* test: repair TypeScript verification baselines

* test(claw-app): prepare WXT aliases before tests

* fix(claw): exclude red from automatic tab-group colors (#2016)

* fix(claw): exclude red from automatic tab groups

* test(claw): update automatic tab color fixture

* test(claw): cover snapshot concurrency contracts (#2017)

* test(claw): add snapshot concurrency fixtures

* test(claw): cover live cursor snapshot acquisition

* test(claw): cover concurrent iframe snapshot contracts

* test(claw): stress overlapping snapshot contracts

* fix: address review findings for snapshot-concurrency-contract-tests

* fix: address review findings for snapshot-concurrency-contract-tests

* refactor: refresh Rust CDP and retire hidden windows (#2013)

* chore(cdp): sync Rust protocol from Chromium

* refactor(core): retire hidden window state

* refactor(mcp): remove hidden window controls

* refactor(server): drop hidden window runtime references

* test(core): align getProperties fixtures with protocol

* test(mcp): retire hidden conformance cases

* chore(release): build v0.48.3 [skip ci]

Automated nightly macOS version bump.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nikhil <nikhilsv92@gmail.com>
Co-authored-by: browseros-bot <bot@browseros.ai>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* feat(claw): advertise full MCP tool catalog

* test(claw): align prompt assertion with main

* chore(claw): refresh generated dispatch model

* chore(claw): sync generated Rust dispatch docs

* chore(claw): sync generated OpenAPI client

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Dani Akash <DaniAkash@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: browseros-bot <bot@browseros.ai>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant